package com.jlf.tool.validation.constraint;

import com.jlf.tool.validation.IdCard;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.regex.Pattern;

/**
 * 身份证号码的有效性校验
 *
 * @author wujr
 * 2021/1/13
 * @version 1.0
 * 变更历史
 * [wujr 2021/1/13 1.0 新增]
 */
public class IdCardConstraintValidator implements ConstraintValidator<IdCard, String> {
    private final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
    private IdCard rule;
    private final Pattern pattern15 = Pattern.compile("^[1-6]\\\\d{5}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)");
    private final Pattern pattern18 = Pattern.compile("^[1-6]\\\\d{5}(18|19|20)\\\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\\\d{3}[0-9X]");
    private final String[] provinceAbbr = {"11","12","13","14","15",
            "21","22","23","31","32","33","34","35","36","37",
            "41","42","43",
            "44","45","46",
            "50","51","52","53","53",
            "61","62","63","64","65",
            "83","81","82"};

    @Override
    public void initialize(IdCard annotation) {
        this.rule = annotation;
    }

    /**
     * 校验身份证号码
     * 1.使用正则表达式校验基本格式
     *
     * （1）地址码：前6位，其中第1位代表大区，我们可以明确知道第1位一定是1-6中的一个数字。后面的5位都是0-9的数字。
     *
     * 故地址码的正则表达式为："^[1-6]\\d{5}"
     *
     * 当然，上面还罗列了所有的省份编号（身份证号码第1，2位），在后面可以通过打表去更严格地校验身份证号码的前2位
     *
     * （2）生日期码
     *
     * ①年：4位，开头两位要么是18（这个可以不需要，因为现在基本上没有是19世纪出生的吧），要么是19，要么是20。后面两位是0-9的数字。正则表达式："(18|19|20)\\d{2}"
     *
     * ②月：2位。分两种情况：①第1位为0，则第2位为数字0-9；②第1位为1，则第2位为0或1或2。正则表达式："((0[1-9])|(1[0-2]))"
     *
     * ③日：2位。第1位可能是0，1，2，第2位可能为1-9的数字（为什么不是0-9，因为考虑错判00为正确的情况）。当然不要漏了特殊的几天：10，20，30，31。由于有些月没有31，且考虑2月的特殊性，在后面仍需进一步校验。正则表达式："(([0-2][1-9])|10|20|30|31)"
     *
     * （3）顺序码：3位。都是0-9的数字。正则表达式："\\d{3}"
     *
     * （4）校验码：校验码是根据前面17位计算出来的，这需要在后面进一步验证。它只能是0-9的数字或者X，x，这我们可以通过正则表达式验证："[0-9X]"。（这里我不写成"[0-9Xx]"的原因见代码）
     *
     * 2.进一步验证
     *
     * （1）我们知道身份证号码的第1，2位代表省份（自治区...），省份也就几十个，全部罗列在上面了，我们不妨打表去进一步校验。较简单，见代码
     *
     * （2）生日期的校验
     *
     * ①身份证号码上面的生日期不能比当前时间还晚（比如说：现在是2019年，身份证号码的生日期是2030年，显然是不对的）
     *
     * ②上面的正则表达式会错判类似以下的情况，比如说：6月31日（事实上6月没有31号，但正则表达式错判为正确）；平年2月29日（平年2月是没有29号的，但正则表达式也错判为正确）。这些情况也需要进一步排除，方法也很简单，也是打表，详情见代码
     *
     * （3）计算校验码
     *
     * 根据前17位计算出相应的检验码（最后一位），如果用户输入的身份证号码的最后一位 与 根据前面17位计算出的校验码不一致，那么这个身份证号码就是错误的
     * @param value 待校验的号码
     * @param context 约束上下文
     * @return true表示通过校验
     */
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (StringUtils.isBlank(value)) {
            return true;
        }
        if (value.length() == 15) {
            // 15位的身份证号码
            if (!rule.i15()) {
                return false;
            }
            if (!pattern15.matcher(value).matches()) {
                return false;
            }
        } else if (value.length() == 18) {
            // 18位的身份证号码
            if (!pattern18.matcher(value).matches()) {
                return false;
            }
        } else {
            return false;
        }
        //第1，2位(省)打表进一步校验

        String province = value.substring(0, 2);
        if (!ArrayUtils.contains(provinceAbbr, province)) {
            return false;
        }
        // 校验生日
        String birthday = value.length() == 15 ? ("19" + value.substring(6, 12)) : value.substring(6, 14);
        Date birth;
        try{
            birth = sdf.parse(birthday);
        }catch (Exception e) {
            // 生日非法
            return false;
        }
        Date now = new Date();

        return birth.getTime() < now.getTime();
    }
}
